﻿
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Sop;

namespace EFCachingProvider.ScalableCache
{
    // Scalable Cache disk mapping routines.
    public partial class ScalableCache
    {
        /// <summary>
        /// Initiate background processing if it is not yet set.
        /// NOTE: background processing is optional. If not set,
        /// each call to member method of this class calls OnIdle
        /// to allow process of expired items in cache.
        /// </summary>
        public void StartBackgroundProcessing(int runIntervalInMilli)
        {
            if (runIntervalInMilli < 500)
                throw new ArgumentOutOfRangeException("runIntervalInMilli", "Minimum runIntervalInMilli is 500.");
            if (_runTask != null) return;
            _runIntervalInMilli = runIntervalInMilli;
            _exitEvent = new AutoResetEvent(false);
            _runTask = new Task(RunLoop);
            _runTask.Start();
        }

        // background run loop.
        private void RunLoop()
        {
            while (_exitEvent.WaitOne(_runIntervalInMilli))
            {
                OnIdle();
            }
        }

        /// <summary>
        /// Signal background processing to end.
        /// </summary>
        public void EndBackgroundProcessing()
        {
            if (_exitEvent == null) return;
            _exitEvent.Set();
            _exitEvent.Dispose();
            _exitEvent = null;
            _runTask.Dispose();
            _runTask = null;
        }

        private bool IsBackgroundProcessing
        {
            get { return _runTask != null; }
        }

        /// <summary>
        /// Number of Entities per batch to expire based on their time stamps in Entity Time Stamp store.
        /// </summary>
        public int ExpireBatchCount
        {
            get { return _expireBatchCount; }
            set
            {
                if (value < 3)
                    throw new ArgumentOutOfRangeException("value", "Minimum expire batch count is 3.");
                _expireBatchCount = value;
            }
        }
        private int _expireBatchCount = 500;

        /// <summary>
        /// Commit Interval.
        /// Data Store is transactional and any changes commited to disk
        /// after every period (CommitInterval value) had elapsed.
        /// </summary>
        public TimeSpan? CommitInterval { get; set; }

        /// <summary>
        /// Method invoked on Idle.
        /// </summary>
        private void MethodOnIdle()
        {
            // if background processing is set, then don't do anything...
            if (IsBackgroundProcessing) return;

            OnIdle();
        }

        /// <summary>
        /// Non re-entrant On Idle processor.
        /// </summary>
        private void OnIdle()
        {
            if (Interlocked.Increment(ref _onIdleCallCtr) == 1)
            {
                ProcessExpiredEntries();
                ProcessBatch();
                Commit();
            }
            Interlocked.Decrement(ref _onIdleCallCtr);
        }

        #region Batch Processor Methods

        private void ProcessBatch()
        {
            var now = GetCurrentDate();
            if (_cacheManager.CurrentBatches.Count < BatchContainer.BatchSize &&
                now.Subtract(_lastProcessBatchDateTime).TotalSeconds < ProcessEntriesIntervalInSec) return;

            _lastProcessBatchDateTime = now;
            BatchContainer batches;
            _cacheManager.CopyClearCache(out batches);
            if (batches == null || batches.IsEmpty) return;

            ProcessBatchInvalidateItems(batches.InvalidSets, batches.InvalidItems);
            ProcessBatchAddItems(batches.AddItems);
        }

        private void ProcessBatchUpdatedTimeStamps(List<KeyValuePair<string, CacheEntry>> updatedTimeStamps,
                                                   bool addOnly = true)
        {
            if (updatedTimeStamps.Count == 0) return;
            // bulk insert items with new expiration time as key
            EntitiesByDate.Locker.Lock();
            foreach (var kvp in updatedTimeStamps)
            {
                kvp.Value.TimeStamp = kvp.Value.ExpirationTime;
                EntitiesByDate.Add(kvp.Value.TimeStamp, kvp.Key);
            }
            EntitiesByDate.Locker.Unlock();
        }

        private void ProcessBatchInvalidateItems(Dictionary<string, byte> invalidSets,
                                                 Dictionary<string, byte> invalidItems)
        {
            if (invalidSets.Count == 0 && invalidItems.Count == 0) return;
            var storeFactory = new StoreFactory();
            foreach (string invalidSet in invalidSets.Keys)
            {
                RemoveEntitySet(storeFactory, invalidSet);
            }

            if (invalidItems.Count == 0) return;
            ProcessInvalidEntities(invalidItems.Keys.ToArray());
        }

        private void ProcessBatchAddItems(Dictionary<string, CacheEntry> addItems)
        {
            if (addItems.Count == 0) return;
            var newItems = new List<KeyValuePair<string, CacheEntry>>(addItems.Count);
            var updatedItems = new List<KeyValuePair<string, CacheEntry>>(addItems.Count);
            var esm = new EntitySetManager();
            Entities.Locker.Lock();
            foreach (var kvp in addItems)
            {
                CacheEntry cacheEntry;
                if (Entities.TryGetValue(kvp.Key, out cacheEntry))
                {
                    // update TimeStamp with existing (on disk) record's TimeStamp to 
                    // keep associated time stamp record in EntitiesByDate store intact.
                    kvp.Value.TimeStamp = cacheEntry.TimeStamp;
                    Entities.CurrentValue = kvp.Value;
                    ProcessDeltasDependentEntitySets(kvp.Key, kvp.Value, cacheEntry, esm);
                }
                else
                {
                    newItems.Add(kvp);
                    if (kvp.Value.DependentEntitySets != null)
                    {
                        foreach (string dependentEntitySet in kvp.Value.DependentEntitySets)
                            esm.SetItem(dependentEntitySet, kvp.Key, true);
                    }
                }
            }
            Entities.Locker.Unlock();
            if (newItems.Count > 0)
            {
                // bulk insert to EntityStore..
                Entities.Locker.Lock();
                for (int i = 0; i < newItems.Count; i++) Entities.Add(newItems[i].Key, newItems[i].Value);
                Entities.Locker.Unlock();
                // create time stamp entries for new Items...
                ProcessBatchUpdatedTimeStamps(newItems.ToList());
            }
            esm.ProcessItems(this);
        }

        private ISortedDictionary<string, string> GetEntitySetStore(string storeName, bool createIfNotFound)
        {
            var storeFactory = new StoreFactory();
            // storeFactory.Get is multi-thread safe, no need to lock any resource...
            ISortedDictionary<string, string> dependentEntityStore = 
                storeFactory.Get<string, string>(Server.SystemFile.Store,
                storeName, createIfNotExist: createIfNotFound);
            return dependentEntityStore;
        }

        // Process deltas on Dependent Entity Sets...
        private void ProcessDeltasDependentEntitySets(string key,
                                                      CacheEntry newEntry, CacheEntry currentEntry,
                                                      EntitySetManager esm)
        {
            var removedDependentEntitySets = new List<string>();
            var addedDependentEntitySets = new List<string>();
            if (currentEntry.DependentEntitySets != null)
            {
                foreach (string dependentEntitySet in currentEntry.DependentEntitySets)
                {
                    if (!newEntry.DependentEntitySets.Contains(dependentEntitySet))
                        removedDependentEntitySets.Add(dependentEntitySet);
                }
            }
            if (newEntry.DependentEntitySets != null)
            {
                foreach (string dependentEntitySet in newEntry.DependentEntitySets)
                {
                    if (!currentEntry.DependentEntitySets.Contains(dependentEntitySet))
                        addedDependentEntitySets.Add(dependentEntitySet);
                }
            }
            foreach (string dependentEntitySet in addedDependentEntitySets)
            {
                esm.SetItem(dependentEntitySet, key, true);
            }
            foreach (string dependentEntitySet in removedDependentEntitySets)
            {
                esm.SetItem(dependentEntitySet, key, false);
            }
        }

        private void RemoveEntitySet(IStoreFactory storeFactory, string dependentEntitySet)
        {
            var dependentEntityStore = GetEntitySetStore(dependentEntitySet, false);
            if (dependentEntityStore == null) return;

            dependentEntityStore.Locker.Lock();
            // delete all items belonging to the entity Set collection.
            if (dependentEntityStore.MoveFirst())
            {
                int ExpireBatchCount = 400;
                var itemsForDelete = new List<QueryExpression<string>>(ExpireBatchCount);
                int ctr = 0;
                do
                {
                    itemsForDelete.Add(new QueryExpression<string>() {Key = dependentEntityStore.CurrentKey});
                    if (++ctr <= ExpireBatchCount) continue;
                    Entities.Locker.Lock();
                    Entities.Remove(itemsForDelete.ToArray());
                    Entities.Locker.Unlock();
                    itemsForDelete.Clear();
                    ctr = 0;
                } while (dependentEntityStore.MoveNext());
                if (ctr > 0)
                {
                    Entities.Locker.Lock();
                    Entities.Remove(itemsForDelete.ToArray());
                    Entities.Locker.Unlock();
                }
            }
            // remove the dependent entity set itself now that all of its items were deleted.
            storeFactory.Remove(Server.SystemFile.Store, dependentEntitySet);
            dependentEntityStore.Locker.Unlock();
            dependentEntityStore.Dispose();
        }

        #endregion

        private void ProcessExpiredEntries()
        {
            var now = GetCurrentDate();
            if (now.Subtract(_lastProcessExpirationDateTime).TotalSeconds < ProcessEntriesIntervalInSec)
                return;

            _lastProcessExpirationDateTime = now;
            // process a batch of expired cache entries...
            int expireBatchCount = ExpireBatchCount;
            var expiredKeys = new List<KeyValuePair<string, DateTime>>(expireBatchCount);
            var notExpiredItems = new List<KeyValuePair<DateTime, string >>(expireBatchCount);
            // remove expired time stamps and get list of related entities for removal.
            int ctr = 0;
            EntitiesByDate.Locker.Lock();
            EntitiesByDate.HintBatchCount = expireBatchCount;
            if (EntitiesByDate.MoveFirst())
            {
                do
                {
                    // check if there are items that expired...
                    if (EntitiesByDate.CurrentKey >= now) break;
                    expiredKeys.Add(new KeyValuePair<string, DateTime>(
                        EntitiesByDate.CurrentValue, EntitiesByDate.CurrentKey));
                    EntitiesByDate.Remove();
                } while (EntitiesByDate.MoveNext() && ctr++ < expireBatchCount);
            }
            EntitiesByDate.HintBatchCount = 0;
            EntitiesByDate.Locker.Unlock();
            // invalidate expired entities, dependent entity sets and entity timestamps...
            if (expiredKeys.Count <= 0) return;
            var esm = new EntitySetManager();
            var forRemove = new List<QueryExpression<string>>(expiredKeys.Count);
            Entities.Locker.Lock();
            foreach (var item in expiredKeys)
            {
                CacheEntry entry;
                if (!Entities.TryGetValue(item.Key, out entry)) continue;
                if (entry.ExpirationTime <= now)
                {
                    forRemove.Add(new QueryExpression<string>() { Key = item.Key });
                    esm.SetItem(item.Key, entry, false);
                    continue;
                }
                entry.TimeStamp = entry.ExpirationTime;
                var nonExpireItem = new KeyValuePair<DateTime, string>(entry.TimeStamp, item.Key);
                notExpiredItems.Add(nonExpireItem);
            }
            Entities.Locker.Unlock();
            if (forRemove.Count <= 0) return;
            // remove expired entities
            Entities.Locker.Lock();
            Entities.Remove(forRemove.ToArray());
            Entities.Locker.Unlock();
            // remove dependent entity sets...
            esm.ProcessItems(this);

            // re-add non-expired entries to timestamp store...
            EntitiesByDate.Locker.Lock();
            foreach (var item in notExpiredItems) EntitiesByDate.Add(item);
            EntitiesByDate.Locker.Unlock();
        }
        private void ProcessInvalidEntities(string[] invalidKeys)
        {
            // package queries and dependent entity sets for remove...
            var forRemove = new List<QueryExpression<string>>(invalidKeys.Length);
            var forRemoveTimeStamps = new List<QueryExpression<DateTime>>(invalidKeys.Length);
            var esm = new EntitySetManager();
            Entities.Locker.Lock();
            foreach (var key in invalidKeys)
            {
                CacheEntry entry;
                if (!Entities.TryGetValue(key, out entry)) continue;
                forRemove.Add(new QueryExpression<string>() { Key = key });
                forRemoveTimeStamps.Add(new QueryExpression<DateTime> { Key = entry.TimeStamp, ValueFilterFunc = v => (string)v == key });
                esm.SetItem(key, entry, false);
            }
            if (forRemove.Count == 0)
            {
                Entities.Locker.Unlock();
                return;
            }
            // remove expired entities
            Entities.Remove(forRemove.ToArray());
            Entities.Locker.Unlock();
            // allow ESM to manage remove of expired items' dependent entity sets...
            esm.ProcessItems(this);

            // remove entities' time stamp records...
            EntitiesByDate.Locker.Lock();
            EntitiesByDate.Remove(forRemoveTimeStamps.ToArray());
            EntitiesByDate.Locker.Unlock();
        }

        private void Commit()
        {
            if (CommitInterval == null ||
                GetCurrentDate().Subtract(_lastCommitDateTime).TotalSeconds < CommitInterval.Value.TotalSeconds) return;
            lock (_serverLocker)
            {
                Server.Commit();
                Server.BeginTransaction();
            }
            _lastCommitDateTime = GetCurrentDate();
        }

        private readonly object _serverLocker = new object();

        private DateTime _lastProcessBatchDateTime;
        private Task _runTask;
        private int _runIntervalInMilli = 5000;
        private AutoResetEvent _exitEvent;
        private long _onIdleCallCtr;
    }
}

